#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <getopt.h>
#include <string.h>
#include <errno.h>

#include "log.h"
#include "misclib.h"

#define DEFAULT_SERVER_SERVICE		"echo"

#define MAXLINE		512

struct G {
	struct {
		const char *program_name; /* 程序名称，即 basename(argv[0]) */
		const char *srv_addr;
		const char *srv_port;
		const char *bind_port;
	} main_args; /* 命令行参数 或者默认值 */
	int sock;
} G = {
		.main_args = {
				.program_name = NULL,
				.srv_addr = NULL,
				.srv_port = DEFAULT_SERVER_SERVICE,
				.bind_port = NULL,
		},
		.sock = -1,
};


static void usage()
{
	printf("Usage: gatewayd [-b<bind port>] domain [service]\n"
			"    domain  Indicates the official name, an alias, or the Internet address \n"
			"            of a server.\n"
			"    service Indicates a service name (or port number).\n"
			"            If not specified, the default "DEFAULT_SERVER_SERVICE
			" is used.\n"
			"Example gatewayd mir.com\n");

    exit(EXIT_SUCCESS);
}

/* 解析命令行参数 */
static void gateway_getopt(int argc, char *argv[]) {
	int c;

	G.main_args.program_name = argc > 0 ? basename(argv[0]) : "";

	while (1) {
		int option_index = 0;
		static struct option long_options[] = {
				{ "help", 0, 0, 0 },
				{ 0, 0, 0, 0 }
		};

		c = getopt_long(argc, argv, "hb:", long_options, &option_index);
		if (c == -1)
			break;

		switch (c) {
		case 0:
			switch (option_index) {
			case 0:
			default:
				usage();
				break;
			}
			break;
		case 'h':
		case '?':
			usage();
			break;
		case 'b':
			G.main_args.bind_port = optarg;
			break;
		default:
			printf("?? getopt returned character code 0%o ??", c);
			break;
		}
	}

	if (optind >= argc) {
		/* 未指定服务器地址 */
		printf("need domain\n");
		usage();
	}

	G.main_args.srv_addr = argv[optind++];

	if (optind < argc)
		G.main_args.srv_port = argv[optind++];

	if (optind < argc) { /* 未识别的命令行参数 */
		printf("non-option ARGV-elements: ");
		while (optind < argc)
			printf("%s ", argv[optind++]);
		printf("\n\n");
		usage();
	}
}

static int connect_retry(int sock, const struct sockaddr *addr, socklen_t alen, int sec)
{
	int nsec;

	/* Try to connect whth exponential backoff */
	for (nsec = 1; nsec <= sec; nsec <<= 1) {
		if (connect(sock, addr, alen) == 0) {
			/* Connetction accepted */
			return 0;
		}

		/* Delay before trying again */
		if (nsec <= sec / 2)
			sleep(nsec);
	}

	return -1;
}

int do_bind(int sock, struct addrinfo *src_aip, const char *service)
{
	struct addrinfo *ailist, *aip;
	struct addrinfo hint;
	int err;

	memset(&(hint), 0, sizeof(hint));
	hint.ai_protocol = src_aip->ai_protocol;
	//hint.ai_flags = src_aip->ai_flags;
	hint.ai_family = src_aip->ai_family;
	hint.ai_socktype = src_aip->ai_socktype;

	if ((err = getaddrinfo(NULL, service, &hint, &ailist)) < 0) {
		LOG(LOG_ERR, "create_listen_socket error: "
				"getaddrinfo(NULL, %s, &hint, &ailist) error: %s",
				service, gai_strerror(err));
		return -1;
	}

	for (aip = ailist; aip; aip = ailist->ai_next) {
		if ((err = bind(sock, aip->ai_addr, aip->ai_addrlen)) == 0)
			break;
	}

	freeaddrinfo(ailist);

	return err;
}

/** create_socket 创建 socket，并连接到服务器
 *
 * 参数：domain 服务器域名 (value)
 *      port 端口 (value)
 *      socktype socket 类型 (value)
 *
 * 返回：正常返回socket文件描述符，错误返回负数 */
int create_socket(const char *domain, const char *port, int socktype, const char *bind)
{
	struct addrinfo *ailist, *aip;
	struct addrinfo hint;
	int	sock = -1;
	int err;

	memset(&(hint), 0, sizeof(hint));
	hint.ai_flags = AI_CANONNAME;
	hint.ai_family = AF_UNSPEC;
	hint.ai_socktype = socktype;

	if ((err = getaddrinfo(domain, port, &hint, &ailist))) {
		LOG(LOG_ERR, "%s: getaddrinfo failed: %s", __FUNCTION__, gai_strerror(err));
		return -1;
	}

	for (aip = ailist; aip != NULL; aip = aip->ai_next) {
		sock = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol);
		if (sock < 0) {
			err = errno;
			continue;
		}

		int reuse = 1;
		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

		if (bind) {
			do_bind(sock, aip, bind);
		}

		if (connect_retry(sock, aip->ai_addr, aip->ai_addrlen, 8) == 0) {
			break;
		}

		err = errno;
		close(sock);
		sock = -1;
	}

	freeaddrinfo(ailist);

	if (sock < 0) {
		LOG(LOG_ERR, "%s: cannot connect to %s,%s: %s",
				__FUNCTION__, domain, port, strerror(err));
	} else {
		log_connect_info(sock);
	}

	return sock;
}

static void str_cli(FILE *fp, int sock)
{
	int fpfd;
	int nfds;
	fd_set rset;
	char buf[MAXLINE];
	int n;

	fpfd = fileno(fp);

	FD_ZERO(&rset);

	while (1) {
		if (fpfd >= 0)
			FD_SET(fpfd, &rset);
		FD_SET(sock, &rset);
		nfds = max(fileno(fp), sock) + 1;
		if (select(nfds, &rset, NULL, NULL, NULL) < 0) {
			LOG(LOG_ERR, "select error: %s\n", strerror(errno));
			exit(EXIT_FAILURE);
		}

		if (FD_ISSET(sock, &rset)) {
			if ((n = read(sock, buf, MAXLINE)) < 0) {
				LOG(LOG_ERR, "read error");
				exit(EXIT_FAILURE);
			} else if (n == 0) {
				if (fpfd < 0) {
					return;
				} else {
					LOG(LOG_ERR, "str_cli: server terminated prematurely\n");
					exit(EXIT_FAILURE);
				}
			}
			if (write(fileno(stdout), buf, n) < 0) {
				LOG(LOG_ERR, "write error");
				exit(EXIT_FAILURE);
			}
		}

		if (fpfd >=0 && FD_ISSET(fileno(fp), &rset)) {
			if ((n = read(fpfd, buf, MAXLINE)) < 0) {
				LOG(LOG_ERR, "read error");
				exit(EXIT_FAILURE);
			} else if (n == 0) {
				if (shutdown(sock, SHUT_WR) < 0) {
					LOG(LOG_ERR, "shutdown error");
					exit(EXIT_FAILURE);
				}
				FD_CLR(fpfd, &rset);
				fpfd = -1;
				continue;
			}
			if (writen(sock, buf, n) != n) {
				LOG(LOG_ERR, "writen error: %s", strerror(errno));
				exit(EXIT_FAILURE);
			}
		}
	}
}

int main(int argc, char *argv[])
{
	gateway_getopt(argc, argv);

#ifdef NDEBUG
	/* Become a daemon. */
	daemonize(basename(argv[0]));
#else /* #ifdef NDEBUG */
	/* Initialize the log file. */
	OPENLOG();
#endif /* #ifdef NDEBUG */

	G.sock = create_socket(G.main_args.srv_addr, G.main_args.srv_port,
			SOCK_STREAM, G.main_args.bind_port);
	if (G.sock < 0)
		goto EXIT;

	str_cli(stdin, G.sock);

EXIT:
	if (G.sock >= 0)
		close(G.sock);

	exit(EXIT_SUCCESS);
}
